package com.project.thisDatabase.mongo;

import com.google.gson.reflect.TypeToken;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.client.*;
import com.mongodb.client.model.Indexes;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import com.project.annotation.enums.common.ConstBaseString;
import com.project.annotation.enums.data.SymbolCode;
import com.project.thisCommon.common.CommonFunction;
import com.project.thisCommon.common.ConvertEntity;
import org.apache.commons.lang3.StringUtils;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.index.TextIndexDefinition;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

@ConditionalOnProperty(name = "spring.data.mongodb.enable", havingValue = "true")
@Component
public class MongoExecutor {

    public final static Integer AES = 1;
    public final static Integer DES = -1;

    @Resource
    public MongoTemplate mongoTemplate;

    public <T> MongoBaseEntity<T> insert(String name, T entity){
        MongoCollection collect = createCollection(name);
        MongoBaseEntity<T> mongo = new MongoBaseEntity<>();
        mongo.setCode(CommonFunction.CreateUUIDString());
        mongo.setBody(entity);
        mongo.setTableName(name);
        mongo.setStatus(ConstBaseString.YES);
        mongo.setCreateTime(CommonFunction.GetNowDateString());
        collect.insertOne(Document.parse(ConvertEntity.javaToJson(mongo)));
        return mongo;
    }

    public <T> void insertAll(String name, T entity){
        MongoCollection collect = createCollection(name);
        collect.insertOne(Document.parse(ConvertEntity.javaToJson(entity)));
    }

    public <T> MongoBaseEntity<T> insert(T entity){
        return insert(entity.getClass().getName(), entity);
    }

    public <T> long delete(T query){
        String name = query.getClass().getName();
        return delete(name, query);
    }

    public <T> long delete(String name, T query){
        MongoCollection collect = createCollection(name);
        DeleteResult deleteResult = collect.deleteMany(createMongoBase(name, query));
        return deleteResult == null ? 0L : deleteResult.getDeletedCount();
    }

    public <T> long deleteAll(String name, T query){
        MongoCollection collect = createCollection(name);
        DeleteResult deleteResult = collect.deleteMany(createMongoBase(query));
        return deleteResult == null ? 0L : deleteResult.getDeletedCount();
    }

    public <T, V> List<MongoBaseEntity<V>> searchBaseEntity(T query){
        return searchBaseEntity(query, new MongoPageEntity());
    }

    public <T, V> List<MongoBaseEntity<V>> searchBaseEntity(T query, MongoPageEntity page){
        if (null == page){
            page = new MongoPageEntity();
        }
        return searchBaseEntity(query.getClass().getName(), query, page);
    }

    public <T, V> List<V> search(T query, MongoPageEntity page){
        if (null == page){
            page = new MongoPageEntity();
        }
        return search(query.getClass().getName(), query, page);
    }

    public <T, V> List<V> search(T query){
        return search(query, null);
    }

    public <T, V> List<V> search(String name, T query, MongoPageEntity page){
        if (null == page){
            page = new MongoPageEntity();
        }
        List<V> result = new ArrayList<>();
        List<MongoBaseEntity<V>> baseEntities = searchBaseEntity(name, query, page);
        baseEntities.forEach((item) ->{
            result.add(item.getBody());
        });

        return result;
    }

    public <T, V> List<V> search(String name, T query, MongoPageEntity page, Type tp){
        if (null == page){
            page = new MongoPageEntity();
        }
        List<V> result = new ArrayList<>();
        List<MongoBaseEntity<V>> baseEntities = searchBaseEntity(name, query, page, tp);
        baseEntities.forEach((item) ->{
            result.add(item.getBody());
        });

        return result;
    }


    public <T> Long count(String name, T query){
        MongoCollection collect = createCollection(name);
        BasicDBObject obj = createMongoBase(name, query);
        return collect.countDocuments(obj);
    }

    public <T> Long countAll(String name, T query){
        MongoCollection collect = createCollection(name);
        BasicDBObject obj = createMongoBase(query);
        return collect.countDocuments(obj);
    }

    public <T> Long countAgg(String name, T query) {
        Map<String, String> m = new TreeMap<>();
        m.put("tableName", "tableName");
        List<MongoAggEntity<Long>> cnt = groupCount(name, query, m);
        for (MongoAggEntity<Long> l : cnt) {
            return l.getAggValue();
        }
        return 0L;
    }

    public <T> Long countAggAll(String name, T query) {
        Map<String, String> m = new TreeMap<>();
        m.put("tableName", "tableName");
        List<MongoAggEntity<Long>> cnt = groupCountAll(name, query, m);
        for (MongoAggEntity<Long> l : cnt) {
            return l.getAggValue();
        }
        return 0L;
    }


    public <T> List<MongoAggEntity<Long>> groupCount(String name, T query, Map<String, String> group){
        BasicDBObject obj = createMongoBase(name, query);
        return innerGroupCount(name, obj, group);
    }

    public <T> List<MongoAggEntity<Long>> groupCountAll(String name, T query, Map<String, String> group){
        BasicDBObject obj = createMongoBase(query);
        return innerGroupCount(name, obj, group);
    }

    protected <T> List<MongoAggEntity<Long>> innerGroupCount(String name, BasicDBObject obj, Map<String, String> group){
        List<MongoAggEntity<Long>> res = new ArrayList<>();
        MongoCollection collect = createCollection(name);
        BasicDBObject group_item = createMongoGroup(group);
        BasicDBObject groupContent = new BasicDBObject("_id", group_item);
        groupContent.put("count", new BasicDBObject("$sum", 1));
        DBObject matchSelect = new BasicDBObject("$match", obj);
        DBObject groupSelect = new BasicDBObject("$group", groupContent);
        List<DBObject> queryList = new ArrayList<>();
        queryList.add(matchSelect);
        queryList.add(groupSelect);

        AggregateIterable<Document> agg = collect.aggregate(queryList);
        MongoCursor<Document> cursor = agg.iterator();
        while(cursor.hasNext()){
            MongoAggEntity en = new MongoAggEntity();
            String json = cursor.next().toJson();
            MongoAggCountEntity countEntity = ConvertEntity.jsonToJava(json, MongoAggCountEntity.class);
            en.setAggValue(countEntity.getCount());
            en.setAggName(countEntity.get_id());
            res.add(en);
        }

        return res;
    }

    public <T, V> List<V> searchAllEntity(String name, T query, MongoPageEntity page){
        BasicDBObject obj = createMongoBase(query);
        Type mongo = new TypeToken<V>() {}.getType();
        return searchInner(name, obj, mongo, page);
    }

    public <T, V> List<MongoBaseEntity<V>> searchBaseEntity(String name, T query, MongoPageEntity page){
        BasicDBObject obj = createMongoBase(name, query);
        Type mongo = new TypeToken<MongoBaseEntity<V>>() {}.getType();
        return searchInner(name, obj, mongo, page);
    }

    public <T, V> List<MongoBaseEntity<V>> searchBaseEntity(String name, T query, MongoPageEntity page, Type tp){
        BasicDBObject obj = createMongoBase(name, query);
        return searchInner(name, obj, tp, page);
    }


    protected <T> List<T> searchInner(String name, BasicDBObject obj, Type tp, MongoPageEntity page) {
        List<T> result = new ArrayList<>();
        MongoCollection collect = createCollection(name);
        FindIterable list = collect.find(Document.parse(obj.toJson()));
        if (page.getSorts().size() > 0){
            BasicDBObject sort = new BasicDBObject();
            page.getSorts().forEach((k, v)->sort.put("body." + k, v));
            list = list.sort(sort);
        }
        if (page.getIndex() > 0){
            list = list.skip(page.getIndex());
        }
        if (page.getSize() > 0){
            list = list.limit(page.getSize());
        }
        MongoCursor<Document> cursor = list.iterator();
        while(cursor.hasNext()){
            String json = cursor.next().toJson();
//            Type mongo = new TypeToken<MongoBaseEntity<T>>() {}.getType();
            T jsonObj = ConvertEntity.jsonToJava(json, tp);
            result.add(jsonObj);
        }

        return result;
    }

    public <T> long update(T query, T entity){
        return update(query.getClass().getName(), query, entity);
    }

    public <T> long update(String name, T query, T entity){
        MongoCollection collect = createCollection(name);
        BasicDBObject queryObj = createMongoBase(name, query);
        BasicDBObject editObj = createMongoBase(name, entity);
        UpdateResult updateResult = collect.updateMany(Document.parse(queryObj.toJson()), Document.parse(editObj.toJson()));
        return updateResult == null ? 0L : updateResult.getModifiedCount();
    }

    public <T> long updateAll(String name, T query, T entity){
        MongoCollection collect = createCollection(name);
        BasicDBObject queryObj = createMongoBase(query);
        BasicDBObject editObj = createMongoBase(entity);
        UpdateResult updateResult = collect.updateMany(Document.parse(queryObj.toJson()), Document.parse(editObj.toJson()));
        return updateResult == null ? 0L : updateResult.getModifiedCount();
    }

    public void createIndex(String name, MongoIndexEntity index) {
        MongoCollection collect = createCollection(name);
        if (MongoSort.desc.equals(index.getType())) {
            collect.createIndex(Indexes.descending(index.getName()));
        } else {
            collect.createIndex(Indexes.ascending(index.getName()));
        }
    }

    public boolean existIndex(String name, String index) {
        ListIndexesIterable<Document> doc = searchIndex(name);
        for (Document document : doc) {
            Object key = document.get("key");
            if (null != key && key instanceof Document) {
                Document keyDocument = (Document) key;
                if (keyDocument.containsKey(index)) {
                    return true;
                }
            }
        }

        return false;
    }

    public ListIndexesIterable<Document> searchIndex(String name) {
        MongoCollection collect = createCollection(name);
        ListIndexesIterable<Document> doc = collect.listIndexes();
        return doc;
    }

    protected <T> BasicDBObject createMongoBase(String name, T query){
        return createMongoBase(name, query, null);
    }

    protected <T> BasicDBObject createMongoBase(String name, T query, String code){
        MongoBaseEntity<T> queryMongo = new MongoBaseEntity<>();
        queryMongo.setCode(code);
        queryMongo.setTableName(name);
        queryMongo.setStatus(ConstBaseString.YES);
        queryMongo.setBody(query);
        BasicDBObject obj = convertMongo(queryMongo);
        return obj;
    }

    protected <T> BasicDBObject createMongoBase(T query){
        BasicDBObject obj = convertMongo(query);
        return obj;
    }

    protected MongoCollection createCollection(String cls){
        if (!mongoTemplate.collectionExists(cls)){
            mongoTemplate.createCollection(cls);
        }

        return mongoTemplate.getCollection(cls);
    }

    protected BasicDBObject convertMongo(Object entity){
        BasicDBObject dbObject = new BasicDBObject();
        Map map;
        if (entity instanceof Map){
            map = (Map)entity;
        } else {
            map = ConvertEntity.CovertToMap(entity);
        }
        convertMongo(StringUtils.EMPTY, map, dbObject);
        return dbObject;
    }

    protected void convertMongo(String key, Map<String, Object> entity, BasicDBObject dbObject){
        for(Map.Entry<String, Object> en : entity.entrySet()){
            String keyVal = StringUtils.isEmpty(key) ? en.getKey() : String.format("%s.%s", key, en.getKey());
            if (en.getValue() instanceof MongoQuery){
                dbObject.put(keyVal, convertMongoQuery((MongoQuery)en.getValue()));
            } else if (en.getValue() instanceof Map){
                convertMongo(keyVal, (Map<String, Object>) en.getValue(), dbObject);
            } else if (en.getValue() instanceof List){
                convertMongoList(keyVal, (List) en.getValue(), dbObject);
            } else if (null == en.getValue()){
                continue;
            } else {
                dbObject.put(keyVal, en.getValue());
            }
        }
    }

    protected BasicDBObject convertMongoQuery(MongoQuery query) {
        BasicDBObject basicDBObject = new BasicDBObject();
        basicDBObject.put(symbolString(query.getSymbol()), query.getValue());
        return basicDBObject;
    }

    protected void convertMongoList(String key, List list, BasicDBObject dbObject){
        for(Object item : list){
            if (item instanceof Map){
                convertMongo(key, (Map<String, Object>) item, dbObject);
            } else if(item instanceof List){
                convertMongoList(key, (List) item, dbObject);
            } else {
                BasicDBObject basicDBList = createMongoIn(list);
                dbObject.put(key, basicDBList);
                break;
            }
        }
    }

    protected BasicDBObject createMongoIn(List list){
        BasicDBList basicDBList = new BasicDBList();
        for(Object item : list){
            basicDBList.add(item);
        }
        BasicDBObject basicDBObject = new BasicDBObject();
        basicDBObject.put("$in", basicDBList);
        return basicDBObject;
    }

    protected BasicDBObject createMongoGroup(Map<String, String> group_map){
        BasicDBObject obj = new BasicDBObject();
        for(String key : group_map.keySet()){
            obj.put(key, "$" + group_map.get(key));
        }

        return obj;
    }

    private String symbolString(int type) {
        if (seq(SymbolCode.EQUAL, type)) {
            if (seq(SymbolCode.LT, type)) {
                return "$lte";
            } else if (seq(SymbolCode.GT, type)) {
                return "$gte";
            } else {
                return "$eq";
            }
        } else if (seq(SymbolCode.NOT, type)) {
            if (seq(SymbolCode.IN, type)) {
                return "$nin";
            } else {
                return "$ne";
            }
        } else if (seq(SymbolCode.GT, type)) {
            return "$gt";
        } else if (seq(SymbolCode.LT, type)) {
            return "$lt";
        } else if (seq(SymbolCode.IN, type)) {
            return "$in";
        }

        return "$eq";
    }

    private boolean seq(int code, int type) {
        return (code & type) == type;
    }
}
